# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2000, 2003 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307	USA

import math
from math import atan2, hypot, pi, sin, cos

from Sketch import Point, Rotation, Translation, Trafo, NullPoint, NullUndo

from blend import Blend, MismatchError, BlendTrafo
import color



class Pattern:

    is_procedural = 1
    is_Empty = 0
    is_Solid = 0
    is_Gradient = 0
    is_RadialGradient = 0
    is_AxialGradient = 0
    is_ConicalGradient = 0
    is_Hatching = 0
    is_Tiled = 0
    is_Image = 0

    name = ''

    def __init__(self, duplicate = None):
        pass

    def SetName(self, name):
        self.name = name

    def Name(self):
        return self.name

    def Execute(self, device, rect = None):
        pass

    def Transform(self, trafo, rects = None):
        # This method is usually called by a primitives Transform method.
        return NullUndo

    def Duplicate(self):
        return self.__class__(duplicate = self)

    Copy = Duplicate

class EmptyPattern_(Pattern):

    is_procedural = 0
    is_Empty = 1

    def Duplicate(self):
        return self

    Copy = Duplicate

    def Blend(self, *args):
        return self

    def SaveToFile(self, file):
        file.EmptyPattern()

    def __str__(self):
        return 'EmptyPattern'

EmptyPattern = EmptyPattern_()

class SolidPattern(Pattern):

    is_procedural = 0
    is_Solid = 1

    def __init__(self, color = None, duplicate = None):
        if duplicate is not None:
            self.color = duplicate.color
        elif color is not None:
            self.color = color
        else:
            raise ValueError,'SolidPattern be must created with color argument'

    def __cmp__(self, other):
        if self.__class__ == other.__class__:
            return cmp(self.color, other.color)
        else:
            return cmp(id(self), id(other))

    def __str__(self):
        return 'SolidPattern(%s)' % `self.color`

    def Execute(self, device, rect = None):
        device.SetFillColor(self.color)

    def Blend(self, other, frac1, frac2):
        if other.__class__ == self.__class__:
            return SolidPattern(Blend(self.color, other.color, frac1, frac2))
        else:
            raise MismatchError

    def Color(self):
        return self.color

    def SaveToFile(self, file):
        file.SolidPattern(self.color)

class GradientPattern(Pattern):

    is_Gradient = 1

    def __init__(self, gradient, duplicate = None):
        if duplicate is not None:
            Pattern.__init__(self, duplicate = duplicate)
            self.gradient = duplicate.gradient.Duplicate()
        elif gradient:
            self.gradient = gradient
        else:
            raise ValueError,\
                  'GradientPattern must be created with gradient argument'

    def Gradient(self):
        return self.gradient

    def SetGradient(self, gradient):
        undo = (self.SetGradient, self.gradient)
        self.gradient = gradient
        return undo


class LinearGradient(GradientPattern):

    is_AxialGradient = 1

    def __init__(self, gradient = None, direction = Point(0, -1),
                 border = 0, duplicate = None):
        GradientPattern.__init__(self, gradient,
                                 duplicate = duplicate)
        self.direction = direction
        self.border = border
        if duplicate is not None:
            if duplicate.__class__ == self.__class__:
                self.direction = duplicate.direction
                self.border = duplicate.border
            elif duplicate.__class__ == ConicalGradient:
                self.direction = duplicate.direction
            elif duplicate.__class__ == RadialGradient:
                self.border = duplicate.border

    def SetDirection(self, dir):
        undo = (self.SetDirection, self.direction)
        self.direction = dir
        return undo

    def Direction(self):
        return self.direction

    def Border(self):
        return self.border

    def SetBorder(self, border):
        undo = (self.SetBorder, self.border)
        self.border = border
        return undo

    def Transform(self, trafo, rects = None):
        dx, dy = self.direction
        dx, dy = trafo.DTransform(dy, -dx)
        dir = Point(dy, -dx).normalized()
        if dir * trafo.DTransform(self.direction) < 0:
            dir = -dir
        return self.SetDirection(dir)

    def Execute(self, device, rect):
        if device.has_axial_gradient:
            self.execute_axial_gradient(device, rect)
            return

        SetFillColor = device.SetFillColor
        FillRectangle = device.FillRectangle
        steps = device.gradient_steps

        colors = self.gradient.Sample(steps)
        SetFillColor(colors[0])
        apply(device.FillRectangle, tuple(rect))

        device.PushTrafo()
        vx, vy = self.direction
        angle = atan2(vy, vx) - pi / 2
        center = rect.center()
        rot = Rotation(angle, center)
        left, bottom, right, top = rot(rect)
        device.Concat(rot)
        device.Translate(center)
        height = top - bottom
        miny = -height / 2
        height = height * (1.0 - self.border)
        width = right - left
        dy = height / steps
        y = height / 2
        x = width / 2
        for i in range(steps):
            SetFillColor(colors[i])
            FillRectangle(-x, y, +x, miny)
            y = y - dy
        device.PopTrafo()

    def execute_axial_gradient(self, device, rect):
        vx, vy = self.direction
        angle = atan2(vy, vx) - pi / 2
        center = rect.center()
        rot = Rotation(angle, center)
        left, bottom, right, top = rot(rect)
        height = (top - bottom) * (1.0 - self.border)
        trafo = rot(Translation(center))
        device.AxialGradient(self.gradient, trafo(0, height / 2),
                             trafo(0, -height / 2))

    def Blend(self, other, frac1, frac2):
        if other.__class__ == self.__class__:
            gradient = other.gradient
            dir = other.direction
            border = other.border
        elif other.__class__ == SolidPattern:
            gradient = other.Color()
            dir = self.direction
            border = self.border
        else:
            raise MismatchError
        return LinearGradient(Blend(self.gradient, gradient, frac1, frac2),
                              frac1 * self.direction + frac2 * dir,
                              frac1 * self.border + frac2 * border)

    def SaveToFile(self, file):
        file.LinearGradientPattern(self.gradient, self.direction, self.border)


class RadialGradient(GradientPattern):

    is_RadialGradient = 1

    def __init__(self, gradient = None, center = Point(0.5, 0.5),
                 border = 0, duplicate = None):
        GradientPattern.__init__(self, gradient,
                                 duplicate = duplicate)
        self.center = center
        self.border = border
        if duplicate is not None:
            if duplicate.__class__ == self.__class__:
                self.center = duplicate.center
                self.border = duplicate.border
            elif duplicate.__class__ == ConicalGradient:
                self.center = duplicate.center
            elif duplicate.__class__ == LinearGradient:
                self.border = duplicate.border

    def SetCenter(self, center):
        undo = (self.SetCenter, self.center)
        self.center = center
        return undo

    def Center(self):
        return self.center

    def Border(self):
        return self.border

    def SetBorder(self, border):
        undo = (self.SetBorder, self.border)
        self.border = border
        return undo

    def Transform(self, trafo, rects = None):
        if rects:
            r1, r2 = rects
            left, bottom, right, top = r1
            cx, cy = self.center
            cx = cx * right + (1 - cx) * left
            cy = cy * top   + (1 - cy) * bottom
            cx, cy = trafo(cx, cy)
            left, bottom, right, top = r2
            len = right - left
            if len:
                cx = (cx - left) / len
            else:
                cx = 0
            len = top - bottom
            if len:
                cy = (cy - bottom) / len
            else:
                cy = 0
            center = Point(cx, cy)
        else:
            center = self.center

        return self.SetCenter(center)

    def Execute(self, device, rect):
        if device.has_radial_gradient:
            self.execute_radial(device, rect)
            return
        steps = device.gradient_steps
        cx, cy = self.center
        cx = cx * rect.right + (1 - cx) * rect.left
        cy = cy * rect.top   + (1 - cy) * rect.bottom
        radius = max(hypot(rect.left - cx, rect.top - cy),
                     hypot(rect.right - cx, rect.top - cy),
                     hypot(rect.right - cx, rect.bottom - cy),
                     hypot(rect.left - cx, rect.bottom - cy))
        color = self.gradient.ColorAt
        SetFillColor = device.SetFillColor
        FillCircle = device.FillCircle
        SetFillColor(color(0))
        apply(device.FillRectangle, tuple(rect))
        radius = radius * (1.0 - self.border)
        dr = radius / steps
        device.PushTrafo()
        device.Translate(cx, cy)
        center = NullPoint
        for i in range(steps):
            SetFillColor(color(float(i) / (steps - 1)))
            FillCircle(center, radius)
            radius = radius - dr
        device.PopTrafo()

    def execute_radial(self, device, rect):
        cx, cy = self.center
        cx = cx * rect.right + (1 - cx) * rect.left
        cy = cy * rect.top   + (1 - cy) * rect.bottom
        radius = max(hypot(rect.left - cx, rect.top - cy),
                     hypot(rect.right - cx, rect.top - cy),
                     hypot(rect.right - cx, rect.bottom - cy),
                     hypot(rect.left - cx, rect.bottom - cy))
        radius = radius * (1.0 - self.border)
        device.RadialGradient(self.gradient, (cx, cy), radius, 0)

    def Blend(self, other, frac1, frac2):
        if other.__class__ == self.__class__:
            gradient = other.gradient
            center = other.center
            border = other.border
        elif other.__class__ == SolidPattern:
            gradient = other.Color()
            center = self.center
            border = self.border
        else:
            raise MismatchError
        return RadialGradient(Blend(self.gradient, gradient, frac1, frac2),
                              frac1 * self.center + frac2 * center,
                              frac1 * self.border + frac2 * border)

    def SaveToFile(self, file):
        file.RadialGradientPattern(self.gradient, self.center, self.border)



class ConicalGradient(GradientPattern):

    is_ConicalGradient = 1

    def __init__(self, gradient = None,
                 center = Point(0.5, 0.5), direction = Point(1, 0),
                 duplicate = None):
        GradientPattern.__init__(self, gradient, duplicate = duplicate)
        self.center = center
        self.direction = direction
        if duplicate is not None:
            if duplicate.__class__ == self.__class__:
                self.center = duplicate.center
                self.direction = duplicate.direction
            elif duplicate.__class__ == LinearGradient:
                self.direction = duplicate.direction
            elif duplicate.__class__ == RadialGradient:
                self.center = duplicate.center

    def __set_center_and_dir(self, center, dir):
        undo = (self.__set_center_and_dir, self.center, self.direction)
        self.center = center
        self.direction = dir
        return undo

    def Transform(self, trafo, rects = None):
        dir = trafo.DTransform(self.direction).normalized()
        if rects:
            r1, r2 = rects
            left, bottom, right, top = r1
            cx, cy = self.center
            cx = cx * right + (1 - cx) * left
            cy = cy * top   + (1 - cy) * bottom
            cx, cy = trafo(cx, cy)
            left, bottom, right, top = r2
            len = right - left
            if len:
                cx = (cx - left) / len
            else:
                cx = 0
            len = top - bottom
            if len:
                cy = (cy - bottom) / len
            else:
                cy = 0
            center = Point(cx, cy)
        else:
            center = self.center

        return self.__set_center_and_dir(center, dir)

    def SetCenter(self, center):
        undo = (self.SetCenter, self.center)
        self.center = center
        return undo

    def Center(self):
        return self.center

    def SetDirection(self, dir):
        undo = (self.SetDirection, self.direction)
        self.direction = dir
        return undo

    def Direction(self):
        return self.direction

    def Execute(self, device, rect):
        if device.has_conical_gradient:
            self.execute_conical(device, rect)
            return
        steps = device.gradient_steps
        cx, cy = self.center
        left, bottom, right, top = rect
        cx = cx * right + (1 - cx) * left
        cy = cy * top	+ (1 - cy) * bottom
        vx, vy = self.direction
        angle = atan2(vy, vx)
        rot = Rotation(angle, cx, cy)
        radius = max(hypot(left - cx, top - cy),
                     hypot(right - cx, top - cy),
                     hypot(right - cx, bottom - cy),
                     hypot(left-cx,bottom-cy)) + 10
        device.PushTrafo()
        device.Concat(rot)
        device.Translate(cx, cy)
        device.Scale(radius)
        colors = self.gradient.Sample(steps)
        SetFillColor = device.SetFillColor
        FillPolygon = device.FillPolygon
        da = pi / steps
        points = [(1, 0)]
        for i in range(steps):
            a = da * (i + 1)
            x = cos(a);	y = sin(a)
            points.insert(0, (x, y))
            points.append((x, -y))
        colors.reverse()
        SetFillColor(colors[0])
        FillPolygon(points)
        points.insert(0, (0, 0))
        for i in range(steps):
            SetFillColor(colors[i])
            del points[1]
            del points[-1]
            FillPolygon(points)
        device.PopTrafo()

    def execute_conical(self, device, rect):
        cx, cy = self.center
        left, bottom, right, top = rect
        cx = cx * right + (1 - cx) * left
        cy = cy * top	+ (1 - cy) * bottom
        angle = self.direction.polar()[1]
        device.ConicalGradient(self.gradient, (cx, cy), angle)

    def Blend(self, other, frac1, frac2):
        if other.__class__ == self.__class__:
            gradient = other.gradient
            dir = other.direction
            center = other.center
        elif other.__class__ == SolidPattern:
            gradient = other.Color()
            dir = self.direction
            center = self.center
        else:
            raise MismatchError
        return ConicalGradient(Blend(self.gradient, gradient, frac1, frac2),
                               frac1 * self.center +  frac2 * center,
                               frac1 * self.direction + frac2 * dir)

    def SaveToFile(self, file):
        file.ConicalGradientPattern(self.gradient, self.center, self.direction)


class HatchingPattern(Pattern):

    is_Hatching = 1

    def __init__(self, foreground = None, background = None,
                 direction = Point(1, 0),
                 spacing = 5.0, width = 0.5, duplicate = None):
        if duplicate is not None:
            self.foreground = duplicate.foreground
            self.background = duplicate.background
            self.spacing = duplicate.spacing
            self.width = duplicate.width
            self.direction = duplicate.direction
        elif foreground:
            self.foreground = foreground
            if not background:
                background = color.StandardColors.white
            self.background = background
            self.spacing = spacing
            self.width = width
            self.direction = direction
        else:
            raise ValueError,\
                  'HatchingPattern must be created with color argument'

    def SetDirection(self, dir):
        undo = (self.SetDirection, self.direction)
        self.direction = dir
        return undo

    def Direction(self):
        return self.direction

    def SetSpacing(self, spacing):
        undo = (self.SetSpacing, self.spacing)
        self.spacing = spacing
        return undo

    def Spacing(self):
        return self.spacing

    def Width(self):
        return self.width

    def Transform(self, trafo, rects = None):
        # XXX: should spacing be transformed as well? Should the pattern be
        # transformed at all?
        dir = trafo.DTransform(self.direction).normalized()
        return self.SetDirection(dir)

    def SetForeground(self, foreground):
        undo = (self.SetForeground, self.foreground)
        self.foreground = foreground
        return undo

    def Foreground(self):
        return self.foreground

    def SetBackground(self, color):
        undo = (self.SetBackground, self.background)
        self.background = color
        return undo

    def Background(self):
        return self.background

    def Execute(self, device, rect):
        left, bottom, right, top = rect
        dy = self.spacing
        if dy > 0:
            device.SetFillColor(self.background)
            device.FillRectangle(left, top, right, bottom)
            device.PushTrafo()
            vx, vy = self.direction
            angle = atan2(vy, vx)
            center = rect.center()
            rot = Rotation(angle, center)
            left, bottom, right, top = rot(rect)
            device.Concat(rot)
            device.Translate(center)
            height = top - bottom
            width = right - left
            steps = int(height / dy + 1)
            y = height / 2
            x = width / 2
            device.SetLineColor(self.foreground)
            device.SetLineAttributes(self.width)
            drawline = device.DrawLineXY
            for i in range(steps):
                drawline(-x, y, +x, y)
                y = y - dy
            device.PopTrafo()
        else:
            device.SetFillColor(self.foreground)
            device.FillRectangle(left, bottom, right, top)

    def Blend(self, other, frac1, frac2):
        if other.__class__ == self.__class__:
            fg = other.foreground
            bg = other.background
            dir = other.direction
            spacing = other.spacing
            width = other.width
        elif other.__class__ == SolidPattern:
            fg = bg = other.Color()
            dir = self.direction
            spacing = self.spacing
            width = self.width
        else:
            raise MismatchError
        return HatchingPattern(Blend(self.foreground, fg, frac1, frac2),
                               Blend(self.background, bg, frac1, frac2),
                               frac1 * self.direction + frac2 * dir,
                               frac1 * self.spacing + frac2 * spacing,
                               frac1 * self.width + frac2 * width)

    def SaveToFile(self, file):
        file.HatchingPattern(self.foreground, self.background,
                             self.direction, self.spacing, self.width)


class ImageTilePattern(Pattern):

    is_Tiled = 1
    is_Image = 1
    data = None

    def __init__(self, data = None, trafo = None, duplicate = None):
        if duplicate is not None:
            data = duplicate.data
            self.trafo = duplicate.trafo
        else:
            if trafo is None:
                #width, height = data.size
                trafo = Trafo(1, 0, 0, -1, 0, 0)
            self.trafo = trafo
        self.data = data

    def set_transformation(self, trafo):
        undo = (self.set_transformation, self.trafo)
        self.trafo = trafo
        return undo

    def Transform(self, trafo, rects = None):
        if rects:
            r1, r2 = rects
            trafo = trafo(Translation(r1.left, r1.top))
            trafo = Translation(-r2.left, -r2.top)(trafo)
        return self.set_transformation(trafo(self.trafo))

    def Execute(self, device, rect):
        device.TileImage(self.data,
                         Translation(rect.left, rect.top)(self.trafo))

    def Blend(self, other, frac1, frac2):
        if self.__class__ == other.__class__:
            if self.data is other.data:
                return self.__class__(self.data,
                                      BlendTrafo(self.trafo, other.trafo,
                                                 frac1, frac2))
        raise MismatchError

    def SaveToFile(self, file):
        file.ImageTilePattern(self.data, self.trafo)
